package org.sercho.masp.notification.decision;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.sercho.masp.distribution.context.ContextConnection;
import org.sercho.masp.models.Context.Display;
import org.sercho.masp.models.Context.InteractionResource;
import org.sercho.masp.models.Context.Loudspeaker;
import org.sercho.masp.models.Context.Place;
import org.sercho.masp.models.Context.User;
import org.sercho.masp.notification.util.NotificationDevice;

/**
 * Class to decide which device is used for the notification.
 * 
 * @author dread
 * 
 */
public class DeviceDecider {

	/**
	 * The angle of a human's direct field of view.
	 */
	private double userDirectViewAngle = 14;
	/**
	 * The angle of a human's whole field of view.
	 */
	private double userViewAngle = 170;
	/**
	 * The angle of the field, from which the content of a display can be seen.
	 */
	private double angleDisplay = 180;

	/**
	 * Constructor.
	 */
	protected DeviceDecider() {
		super();
	}

	/**
	 * Method to decide which {@link InteractionResource}s will be used.
	 * 
	 * @param userId
	 *            user for the notification
	 * @param notificationMethod
	 *            Method id (1,2,3,4,5)
	 * @param context
	 *            the {@link ContextConnection} to the context-model
	 * @return List of {@link InteractionResource} which are practicable for the
	 *         notification method. Returns an empty {@link List}, if no
	 *         {@link InteractionResource} was found or chosen.
	 * 
	 */
	protected List<InteractionResource> getNotificationDevices(
			ContextConnection context, String userId, int notificationMethod) {

		List<InteractionResource> resultList = new ArrayList<InteractionResource>();

		User user = context.getUser(userId);
		Place userPlace = user.getPlace();

		EList<InteractionResource> resources = userPlace
				.getInteractionResources();

		switch (notificationMethod) {

		// no notification
		case 1:
			return resultList;

			// visual non direct notification
		case 2:
		case 3:
			List<InteractionResource> visualTempList = new ArrayList<InteractionResource>();
			List<InteractionResource> audioTempList = new ArrayList<InteractionResource>();

			// getting all visual and audio interaction ressources
			for (InteractionResource ir : resources) {
				if (ir instanceof Display) {
					visualTempList.add(ir);
				}
				if (ir instanceof Loudspeaker) {
					audioTempList.add(ir);
				}
			}

			// if visual interaction resource is found, try to get a nondirect
			List<InteractionResource> directVisualIr = new ArrayList<InteractionResource>();
			List<InteractionResource> nondirectVisualIr = new ArrayList<InteractionResource>();
			directVisualIr = this.getInteractionResourceFilteredByRange(
					visualTempList, user, this.userDirectViewAngle);
			nondirectVisualIr = this.getInteractionResourceFilteredByRange(
					visualTempList, user, this.userViewAngle);

			if (!nondirectVisualIr.isEmpty()) {
				return nondirectVisualIr;
			} else if (!directVisualIr.isEmpty()) {
				return directVisualIr;
			}

			System.out
					.println(">>>>>>> NOTIFICATION-SYSTEM: No Display Found, fall back to audio");
			// fallback: if no display was found, get an audio interaction
			// resource
			audioTempList = this.getInteractionResourceFilteredByRange(
					audioTempList, user, 0);
			return audioTempList;

			// visual direct, audio notification
		case 5:
		case 4:
			resultList = this.getDirectInteractionResourcesFilterByRange(
					resources, user);

		}

		return resultList;
	}

	/**
	 * Method to check if a user can see a display, based in coordinates and
	 * view vectors
	 * 
	 * @param angleUser
	 *            visual angle of the users view in degree(usually about 170
	 *            degrees)
	 * @param user
	 *            the {@link User}
	 * @param ir
	 *            the {@link InteractionResource}, which is checked for being
	 *            seen by the {@link User}
	 * @return returns true if the user is able to see the display, false if not
	 *         or the {@link InteractionResource} is no {@link Display}
	 */
	private boolean isUserAbleToSeeDisplay(double angleUser, User user,
			InteractionResource ir) {

		if (!(ir instanceof Display)) {
			return false;
		}

		Display display = (Display) ir;

		System.out.println("Checking Display: " + display.getName());

		if (display.getDirection() != null && display.getPosition() != null) {
			double[] userPos = { user.getPosition().getX(),
					user.getPosition().getY() };
			double[] devicePos = { ir.getPosition().getX(),
					ir.getPosition().getY() };
			double[] userViewVector = { user.getLineOfVision().getX(),
					user.getLineOfVision().getY() };
			double[] deviceOrientationVector = { display.getDirection().getX(),
					display.getDirection().getY() };

			System.out.println("Displaypos: " + devicePos[0] + ","
					+ devicePos[1]);
			System.out.println("DisplayLOV: " + deviceOrientationVector[0]
					+ "," + deviceOrientationVector[1]);
			System.out.println("Userpos: " + userPos[0] + "," + userPos[1]);
			System.out.println("UserposLOV: " + userViewVector[0] + ","
					+ userViewVector[1]);
			System.out.println("UserViewAngle: " + angleUser + "°");

			boolean userCanSeeDevice = false;
			boolean deviceCanSeeUser = false;

			// checking if the user can see the device screen
			double[] vectorUserToDevice = { devicePos[0] - userPos[0],
					devicePos[1] - userPos[1] };

			// Compute the angle between the the user's line of vision and the
			// distance-vector between the user and the display.
			double angleOfUserToDevice = Math.acos((vectorUserToDevice[0]
					* userViewVector[0] + vectorUserToDevice[1]
					* userViewVector[1])
					/ (Math.sqrt(Math.pow(vectorUserToDevice[0], 2)
							+ Math.pow(vectorUserToDevice[1], 2)) * Math
							.sqrt(Math.pow(userViewVector[0], 2)
									+ Math.pow(userViewVector[1], 2))));
			System.out
					.println("Angle between UserLOV and UserToDevicePosVector: "
							+ Math.toDegrees(angleOfUserToDevice));

			// If the angle is smaller than half of the given user's view-angle,
			// the user can see the device. It's the half of the angle, because
			// the field of view is divided in halfs by the line of vision.
			if (Math.toDegrees(angleOfUserToDevice) <= angleUser / 2) {
				userCanSeeDevice = true;
				// When we don't check for the direct-view-angle, we only want
				// displays to be found, that are in the peripheral field of
				// view. Therefore, when the given user-angle is not the
				// direct-view-angle, we assert that the display is not seen, if
				// it resides in the direct field of view.
				if (angleUser != this.userDirectViewAngle) {
					if (Math.toDegrees(angleOfUserToDevice) <= this.userDirectViewAngle / 2) {
						userCanSeeDevice = false;
					}
				}
			}
			System.out.println("User can see device: " + userCanSeeDevice);

			// checking if the device can see the user
			double[] vectorDeviceToUser = { userPos[0] - devicePos[0],
					userPos[1] - devicePos[1] };
			// Now we compute the angle between the direction-vector of the
			// display and the distance-vector between the display and the user.
			double angleOfDeviceToUser = Math
					.acos((vectorDeviceToUser[0] * deviceOrientationVector[0] + vectorDeviceToUser[1]
							* deviceOrientationVector[1])
							/ (Math.sqrt(Math.pow(vectorDeviceToUser[0], 2)
									+ Math.pow(vectorDeviceToUser[1], 2)) * Math
									.sqrt(Math.pow(deviceOrientationVector[0],
											2)
											+ Math.pow(
													deviceOrientationVector[1],
													2))));
			System.out
					.println("Angle between DeviceLOV and DeviceToUserPosVector: "
							+ Math.toDegrees(angleOfDeviceToUser));
			// If the angle is smaller than half of the given display-angle,
			// the user can read from the display. It's the half of the angle,
			// because
			// the display-angle is divided in halfs by the direction-vector.
			if (Math.toDegrees(angleOfDeviceToUser) <= angleDisplay / 2) {
				deviceCanSeeUser = true;
			}
			System.out.println("Device can see user: " + deviceCanSeeUser);

			return userCanSeeDevice && deviceCanSeeUser;
		}

		return false;
	}

	/**
	 * Removes all interaction resources from irList which can not be seen and
	 * read by the user if they are visual. The given view-angle defines the
	 * field of view, in which a visual device have to be. If this angle is not
	 * the direct-view-angle, visual devices in the direct field of view will
	 * also be removed. Finally filters the interaction resources by range. Only
	 * the nearest {@link InteractionResource} will be returned in a
	 * {@link List}.
	 * 
	 * @param irList
	 *            List with {@link InteractionResource} to check. Should contain
	 *            only {@link Display} or {@link Loudspeaker}
	 * @param user
	 *            User
	 * @param viewAngleUser
	 *            view angle of user in degree
	 * @return nearest interaction resource in a {@link List} of
	 *         {@link InteractionResource}
	 */
	private List<InteractionResource> getInteractionResourceFilteredByRange(
			List<InteractionResource> irList, User user, double viewAngleUser) {

		List<InteractionResource> tempListAll = new ArrayList<InteractionResource>();

		for (InteractionResource ir : irList) {
			// if resource is a display we need to check whether the user can
			// see the display
			if (ir instanceof Display) {
				Display display = (Display) ir;
				// checking if user is able to see the display
				if (isUserAbleToSeeDisplay(viewAngleUser, user, display)) {
					tempListAll.add(ir);
				}
			}
			// if resource is a loudspeaker just add as possible device (this is
			// a fallback if no visual device was found for method 2 or 3)
			if (ir instanceof Loudspeaker) {
				tempListAll.add(ir);
			}
		}

		// get interaction resource with lowest distance to user
		List<InteractionResource> resultList = this
				.getNearbyInteractionResource(tempListAll, user);

		return resultList;
	}

	/**
	 * Method to get {@link InteractionResource}s for method 4 and 5.
	 * 
	 * @param irList
	 *            {@link List} of {@link InteractionResource} to filter
	 * @param user
	 *            User
	 * @return {@link List} of {@link InteractionResource} to use for method 4
	 *         or 5.
	 */
	private List<InteractionResource> getDirectInteractionResourcesFilterByRange(
			List<InteractionResource> irList, User user) {

		List<InteractionResource> resultList = new ArrayList<InteractionResource>();

		// List of devices with both, direct visual and audio notification
		List<NotificationDevice> videoAudioDeviceList = this
				.lookForAudioAndVideoDevices(irList, user,
						this.userDirectViewAngle);

		// first fallback: if no devices with direct view were found, look for
		// devices with non direct
		// view
		if (videoAudioDeviceList.isEmpty()) {
			videoAudioDeviceList = this.lookForAudioAndVideoDevices(irList,
					user, this.userViewAngle);
		}

		// filter devices by range to get the nearest one to the user
		resultList = this.getNearbyDeviceInteractionResource(
				videoAudioDeviceList, user);

		// second fallback: if no devices found with audio and video interaction
		// resource look for single devices
		if (videoAudioDeviceList.isEmpty()) {

			List<InteractionResource> tempAudioList = new ArrayList<InteractionResource>();
			List<InteractionResource> tempVisualList = new ArrayList<InteractionResource>();

			for (InteractionResource ir : irList) {
				if (ir instanceof Display) {
					tempVisualList.add(ir);
				}
				if (ir instanceof Loudspeaker) {
					tempAudioList.add(ir);
				}
			}

			// get nearest visual interaction resource ( with fallback to
			// nondirect visual )
			List<InteractionResource> visualIrList = this
					.getInteractionResourceFilteredByRange(tempVisualList,
							user, this.userDirectViewAngle);
			if (visualIrList.isEmpty()) {
				visualIrList = this.getInteractionResourceFilteredByRange(
						tempVisualList, user, this.userViewAngle);
			}

			// get nearest audio interaction resource
			List<InteractionResource> audioIrList = this
					.getInteractionResourceFilteredByRange(tempAudioList, user,
							0);

			// merge visual and audio interaction resource list
			visualIrList.addAll(audioIrList);

			resultList = visualIrList;
		}

		return resultList;
	}

	/**
	 * 
	 * Method to filter {@link InteractionResource}s by their range to the user.
	 * 
	 * @param irList
	 *            List of {@link InteractionResource} to filter by range to the
	 *            user.
	 * @param user
	 *            User
	 * @return List of {@link InteractionResource} contains an
	 *         {@link InteractionResource} which is nearest to user. List is
	 *         empty if no {@link InteractionResource} was found.
	 */
	private List<InteractionResource> getNearbyInteractionResource(
			List<InteractionResource> irList, User user) {

		List<InteractionResource> resultList = new ArrayList<InteractionResource>();
		InteractionResource nearestIR = null;
		double distance = Double.MAX_VALUE;

		for (InteractionResource ir : irList) {
			if (nearestIR == null
					|| distance > Point.distance(user.getPosition().getX(),
							user.getPosition().getY(), ir.getPosition().getX(),
							ir.getPosition().getY())) {
				nearestIR = ir;
				distance = Point.distance(user.getPosition().getX(), user
						.getPosition().getY(), ir.getPosition().getX(), ir
						.getPosition().getY());
			}
		}

		if (nearestIR != null) {
			resultList.add(nearestIR);
		}

		return resultList;
	}

	/**
	 * 
	 * Method to filter devices by their range to the user.
	 * 
	 * @param devList
	 *            List of {@link NotificationDevice} to filter by range to the
	 *            user.
	 * @param user
	 *            User
	 * @return List of {@link InteractionResource} contains all audio and visual
	 *         {@link InteractionResource}s (which are in the same device) which
	 *         are nearest to user. List is empty if no
	 *         {@link InteractionResource} was found.
	 */
	private List<InteractionResource> getNearbyDeviceInteractionResource(
			List<NotificationDevice> devList, User user) {

		List<InteractionResource> resultList = new ArrayList<InteractionResource>();
		NotificationDevice nearestDev = null;
		double distance = Double.MAX_VALUE;

		for (NotificationDevice dev : devList) {
			if (nearestDev == null
					|| distance > Point.distance(user.getPosition().getX(),
							user.getPosition().getY(), dev.getVisualIrList()
									.get(0).getPosition().getX(), dev
									.getVisualIrList().get(0).getPosition()
									.getY())) {
				nearestDev = dev;
				distance = Point.distance(user.getPosition().getX(), user
						.getPosition().getY(), dev.getVisualIrList().get(0)
						.getPosition().getX(), dev.getVisualIrList().get(0)
						.getPosition().getY());
			}
		}

		if (nearestDev != null) {
			resultList.addAll(nearestDev.getVisualIrList());
			resultList.addAll(nearestDev.getAudioIrList());
		}

		return resultList;
	}

	/**
	 * 
	 * Method to get all devices which have an audio and visual
	 * {@link InteractionResource} and visible to user.
	 * 
	 * @param user
	 *            User
	 * @param viewAngleUser
	 *            view angle of the user
	 * @param irList
	 *            {@link List} with all {@link InteractionResource}s to find
	 *            possible combinations for.
	 * @return {@link List} of {@link NotificationDevice}. List is empty if no
	 *         device was found.
	 */
	private List<NotificationDevice> lookForAudioAndVideoDevices(
			List<InteractionResource> irList, User user, double viewAngleUser) {

		List<NotificationDevice> deviceList = new ArrayList<NotificationDevice>();

		for (InteractionResource ir : irList) {
			if (ir instanceof Display) {
				Display display = (Display) ir;
				// looking for a visual interaction resource the user can
				// see
				if (isUserAbleToSeeDisplay(viewAngleUser, user, display)) {
					// create a NotificationDevice for temporal storage
					NotificationDevice ndevice = new NotificationDevice();
					ndevice.addVisualIr(ir);

					// looking for an audio interaction resource for this
					// device
					for (InteractionResource ir2 : irList) {
						if (ir2 instanceof Loudspeaker
								&& ir.getDevice() == ir2.getDevice()) {
							ndevice.addAudioIr(ir2);
							if (!deviceList.contains(ndevice)) {
								deviceList.add(ndevice);
							}
						}
					}
				}
			}
		}
		return deviceList;
	}
}
